Skip to content

Add Custom Media Source Integration via IPC Hooks#1727

Open
higorprado wants to merge 2 commits intoAvengeMedia:masterfrom
higorprado:feature/custom-media-source
Open

Add Custom Media Source Integration via IPC Hooks#1727
higorprado wants to merge 2 commits intoAvengeMedia:masterfrom
higorprado:feature/custom-media-source

Conversation

@higorprado
Copy link
Contributor

Summary

Add support for external media players (rmpc, cmus, mpv, etc.) to integrate with DMS media widgets through IPC hooks, enabling non-MPRIS players to appear alongside native MPRIS players in the bar, dash, and OSD.

Problem

DMS media widgets only support MPRIS-compatible players. Many popular terminal-based music players (rmpc/MPD, cmus, moc, etc.) don't expose MPRIS interfaces, leaving users without media controls in the shell UI.

Solution

Implement a custom media source that receives metadata updates via IPC hooks and executes shell commands for playback control. The system integrates transparently with existing MPRIS infrastructure.

Architecture

CustomMediaSource.qml

A singleton that mirrors the MprisPlayer interface:

  • Stores media state (track info, playback state, position, etc.)
  • Receives updates via media.update IPC command
  • Executes configured shell commands for play/pause/next/previous
  • Emits signals for OSD and widget change detection

MprisController.qml

Extended as the unified media interface:

  • currentPlayer property returns either MPRIS player or CustomMediaSource
  • Direct property bindings (currentIsPlaying, currentTrackTitle, etc.) for reliable UI updates
  • Auto-selection logic: custom source takes precedence when it has content loaded
  • Manual source selection via dropdown for user override

Widget Updates

All media widgets now use MprisController.currentPlayer:

  • Bar media widget (play/pause, track scrolling)
  • Dash media tab (full controls, seekbar, album art)
  • Media OSD (playback state notifications)
  • Volume OSD (per-player volume control)

IPC Commands

New media target for external integrations:

Command Description
media.update <json> Push track metadata and state
media.setCommands <json> Configure playback shell commands
media.status Query current state
media.play Trigger play command
media.pause Trigger pause command
media.playPause Trigger toggle command
media.next Trigger next track command
media.previous Trigger previous track command
media.clear Clear media state

Integration Example

External tools integrate by calling IPC from their hooks. Example with rmpc:

~/.config/rmpc/dms_hook:

#!/bin/bash
# Set playback commands (idempotent - safe to call every time)
dms ipc call media setCommands '{"play":"rmpc play","pause":"rmpc pause","toggle":"rmpc togglepause","next":"rmpc next","prev":"rmpc prev"}'

# Push current track info to DMS
dms ipc call media update "{\"title\":\"$TITLE\",\"artist\":\"$ARTIST\",\"album\":\"$ALBUM\",\"duration\":$DURATION,\"state\":1,\"available\":true,\"sourceId\":\"rmpc\"}"

~/.config/rmpc/config.ron:

on_song_change: [
    "~/.config/rmpc/dms_hook"
]

The hook pattern ensures commands are always available even after DMS restarts.

Changes

File Change
Services/CustomMediaSource.qml New - Custom media source singleton
Services/MprisController.qml Extended - Unified player interface with direct property bindings
DMSShellIPC.qml Extended - Add media IPC target
Modules/DankBar/Widgets/Media.qml Updated - Use unified interface
Modules/DankDash/MediaPlayerTab.qml Updated - Use unified interface
Modules/DankBar/Widgets/AudioVisualization.qml Updated - Use unified interface
Modules/DankDash/Overview/MediaOverviewCard.qml Updated - Use unified interface
Widgets/DankSeekbar.qml Updated - Use unified interface
Other media-related widgets Updated - Consistent use of unified interface
docs/IPC.md Extended - Documentation for media target

Testing

Tested with rmpc (MPD client):

  • Hook fires on song change → DMS receives metadata
  • Play/pause button toggles state and updates UI immediately
  • Next/previous buttons skip tracks
  • Album art displays correctly
  • Auto-selection keeps custom source active even when paused
  • Manual source selection via player dropdown works
  • MPRIS players (Firefox, Spotify) still work alongside custom source

Enable external media players (rmpc, mpv, cmus, etc.) to push metadata
to DMS through IPC hooks, allowing non-MPRIS players to appear in the
media widgets alongside native MPRIS players.

Changes:
- Add CustomMediaSource.qml singleton for external media state
- Add unified currentPlayer interface in MprisController that
  transparently switches between MPRIS and custom sources
- Update media widgets (bar, dash, OSD) to use currentPlayer
- Add IPC commands: media update, setCommands, clear, status
- Add IPC documentation for the media target

External tools can integrate by calling the IPC from their hooks:
  dms ipc call media setCommands '{"play":"rmpc play",...}'
  dms ipc call media update '{"title":"...","artist":"...",...}'
}

function _exec(cmd) {
if (cmd) Quickshell.execDetached(["sh", "-c", cmd])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a little risky that shell commands could potentially be injected into the config, but I think it's a low risk.


Loader {
active: activePlayer?.playbackState === MprisPlaybackState.Playing && showAnimation
active: activePlayer?.playbackState === 1 && showAnimation
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why replace the enum here with numbers?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, using magic numbers is not ideal. I've replaced them with CustomMediaSource.statePlaying/stateStopped/statePaused constants. These work for both MprisPlayer and CustomMediaSource since both use the same numeric values (0, 1, 2). This keeps the code readable while maintaining compatibility with the unified media interface.

id: root

property MprisPlayer activePlayer
property var activePlayer
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get the rationale I guess for making this a generic type, but I'm not too keen on losing the QML type here. Maybe a second property or a wrapped QtObject

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using var is a pragmatic solution for polymorphism in QML. The activePlayer property can be either an MprisPlayer or CustomMediaSource, and QML doesn't support union types. Both types expose the same interface (playbackState, trackTitle, trackArtUrl, etc.), so a wrapper or dual properties would add complexity without practical benefit.

readonly property bool useCustomSource: {
if (forcedSource === "custom") return CustomMediaSource.available
if (forcedSource === "mpris") return false
// Auto: use custom when it has content (track loaded), regardless of playing state
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we want to use it regardless of playing state?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intentional behavior. External players like rmpc or cmus pause frequently while users work. When paused, users still expect to see their current track in the UI. If we only used the custom source when actively playing, the UI would switch away to another MPRIS player (or show nothing) every time the user paused — which would be confusing. The customHasContent check means "a track is loaded", not "track is playing".

Replace magic numbers (0, 1, 2) with CustomMediaSource constants
(stateStopped, statePlaying, statePaused) for better readability
and consistency across the unified media interface.
@higorprado higorprado force-pushed the feature/custom-media-source branch from dd183e6 to baee57f Compare February 20, 2026 12:22
@higorprado
Copy link
Contributor Author

higorprado commented Feb 20, 2026

Thank you for the review.

Regarding the enum vs magic numbers

You're right, using magic numbers is not ideal. I've replaced them with CustomMediaSource.statePlaying/stateStopped/statePaused constants. These work for both MprisPlayer and CustomMediaSource since both use the same numeric values (0, 1, 2). This keeps the code readable while maintaining compatibility with the unified media interface.

Regarding the var type for activePlayer

Using var is a pragmatic solution for polymorphism in QML. The activePlayer property can be either an MprisPlayer or CustomMediaSource, and QML doesn't support union types. Both types expose the same interface (playbackState, trackTitle, trackArtUrl, etc.), so a wrapper or dual properties would add complexity without practical benefit.

Regarding "use custom source regardless of playing state"

This is intentional behavior. External players like rmpc or cmus pause frequently while users work. When paused, users still expect to see their current track in the UI. If we only used the custom source when actively playing, the UI would switch away to another MPRIS player (or show nothing) every time the user paused — which would be confusing. The customHasContent check means "a track is loaded", not "track is playing".


Loader {
active: activePlayer?.playbackState === MprisPlaybackState.Playing && showAnimation
active: activePlayer?.playbackState === CustomMediaSource.statePlaying && showAnimation
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I am wondering is why cant we re-use the MprisPlaybackState enum that is built into quickshell, instead of having a separate one or fixed numbers? We can re-purpose it for our custom media player values so it matches, same enum.

forcedSource = "mpris"
activePlayer = source.player
} else {
forcedSource = "auto"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no path back to "auto" - selectSource seems like it's always called with an argument

if (artist.length > 0)
return artist + (isActive ? " (Active)" : "");
return isActive ? "Active" : "Available";
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We lose the artist data here yea?

}

function _exec(cmd) {
if (cmd) Quickshell.execDetached(["sh", "-c", cmd])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like instead of this, we should just have a list of known players and commands like rpmc, that we can add to later. Feels a little risky to allow it this way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants